import toml
import pathlib
import os
import re
import sys
import shutil
import tomllib
import semver

from version_condition import VersionCondition


ROOT_CRATE_PATH: pathlib.Path = pathlib.Path(".")
DEPS_PATH: pathlib.Path = pathlib.Path("../deps")


class RustCrateInfo:
    """Cargo.toml中记录的crate的信息，包含名字、版本号要求和版本额外信息"""

    def __init__(self, crate_name: str, version: VersionCondition):
        self.crate_name = crate_name
        self.version = version

    @staticmethod
    def from_cargo_vendor_format(dirname_txt: str) -> "RustCrateInfo":
        """
        从形如wasi-0.11.0+wasi-snapshot-preview1这样的字符串中提取crate信息。
        """
        version_num_str = re.search(r"\d+\.\d+\.\d+", dirname_txt)
        assert version_num_str is not None
        version_full_str = version_num_str.group()
        return RustCrateInfo(
            dirname_txt[: version_num_str.start() - 1],
            VersionCondition.from_version_str(version_full_str),
        )

    @staticmethod
    def from_cargo_toml_dep_format(
        crate_name: str, version_str: str
    ) -> "RustCrateInfo":
        """
        从读取Cargo.toml中读取的 `dependencies` 中条目提取crate信息。
        """
        return RustCrateInfo(crate_name, VersionCondition.from_version_str(version_str))

    def can_be_replace_by(
        self, crate_name: str, crate_semver: semver.Version, extra_info: str
    ) -> bool:
        """
        若other可替换self，返回True。[规则参考](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html)

        按照cargo的规定，当且仅当other没更改self最左边的那个非零数字时，可认为other可替换self。
        """
        return self.crate_name == crate_name and self.version.can_be_satisfied_by(
            crate_semver, extra_info
        )

    def find_crate_dir(self) -> pathlib.Path | None:
        """
        给定Rust Crate的名字和版本，找到其在 `DEPS_PATH` 目录下的路径。如果没有找到，返回None。

        一般来说，没找到说明 `cargo vendor` 发现这个Crate压根没用上，因此全是死代码。
        """
        global DEPS_PATH
        for dirname in os.listdir(DEPS_PATH):
            # 从目录中找到的crate名字和版本号一定是准确且完整的
            each_crate_ver = re.search(r"\d+\.\d+\.\d+", dirname)
            assert each_crate_ver is not None
            each_crate_name = dirname[: each_crate_ver.start() - 1]
            each_semver = each_crate_ver.group()
            each_extra_info = dirname[each_crate_ver.end() :]
            if self.can_be_replace_by(
                each_crate_name, semver.Version.parse(each_semver), each_extra_info
            ):
                return DEPS_PATH / dirname
        return None

    def __repr__(self) -> str:
        return f"RustCrateInfo({self.crate_name}, {self.version})"


def process(cargo_toml_path: pathlib.Path):
    # 备份原Cargo.toml。若已经有备份，则视为该crate已被处理完成，不再另行处理
    supposed_backup = cargo_toml_path.with_suffix(cargo_toml_path.suffix + ".bak")
    if supposed_backup.exists():
        print(f"Skipping {cargo_toml_path}...")
        return
    print(f"Processing {cargo_toml_path}...")

    crate_path = cargo_toml_path.parent.absolute()
    with open(cargo_toml_path, "rb") as f:
        data = tomllib.load(f)
    try:
        # 不一定有dependencies，如果没有的话直接跳过就好
        if deps := data.get("dependencies", False):
            for dep_name in deps.keys():
                dep_name: str
                if isinstance(deps[dep_name], str):
                    deps[dep_name] = {"version": deps[dep_name]}
                if deps[dep_name].get("path", False):
                    continue  # 跳过本地路径依赖
                dep_version_str: str = deps[dep_name]["version"]
                dep_info = RustCrateInfo.from_cargo_toml_dep_format(
                    dep_name, dep_version_str
                )
                crate_dir = dep_info.find_crate_dir()
                # print(dep_info, "at path:", crate_dir)
                if crate_dir is not None:
                    deps[dep_name]["path"] = str(
                        crate_dir.absolute().relative_to(crate_path, walk_up=True)
                    )
                    del deps[dep_name]["version"]
        # 不一定有dev-dependencies，如果没有的话直接return就好
        if dev_deps := data.get("dev-dependencies", False):
            for dep_name in dev_deps.keys():
                dep_name: str
                if isinstance(dev_deps[dep_name], str):
                    dev_deps[dep_name] = {"version": dev_deps[dep_name]}
                if dev_deps[dep_name].get("path", False):
                    continue  # 跳过本地路径依赖
                dep_version_str: str = dev_deps[dep_name]["version"]
                dep_info = RustCrateInfo.from_cargo_toml_dep_format(
                    dep_name, dep_version_str
                )
                crate_dir = dep_info.find_crate_dir()
                # print(dep_info, "at path:", crate_dir)
                if crate_dir is not None:

                    dev_deps[dep_name]["path"] = str(
                        crate_dir.absolute().relative_to(crate_path, walk_up=True)
                    )
                    del dev_deps[dep_name]["version"]
    except Exception as e:
        raise e

    shutil.copy(
        cargo_toml_path,
        supposed_backup,
    )
    with open(cargo_toml_path, "w") as f:
        toml.dump(data, f)


process(ROOT_CRATE_PATH / "Cargo.toml")

for each in os.listdir(DEPS_PATH):
    each_path = DEPS_PATH / each
    if each_path.is_dir():
        process(each_path / "Cargo.toml")
